﻿using System;
using System.Collections.Generic;
using System.Reflection;
using MugenInjection.Attributes;
using MugenInjection.Core;
using MugenInjection.Exceptions;
using MugenInjection.Infrastructure;
using MugenInjection.Interface;

namespace MugenInjection.Activators
{
    /// <summary>
    ///     Represents the base class to create and inject values into the specified object.
    /// </summary>
    public abstract class ActivatorBase : DisposableObject, IActivator
    {
        #region Fields

        private bool _isEmpty;
        private CacheInfoModel<MethodInfo> _cachedMethodModel;
        private CacheInfoModel<PropertyInfo> _cachedPropertyModel;
#if !NETFX_CORE
        private BindingFlags _bindingFlags;
#endif

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="ActivatorBase"/> class.
        /// </summary>
        protected ActivatorBase()
        {
#if !NETFX_CORE
            BindingFlags =
                BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod |
                BindingFlags.SetField | BindingFlags.SetProperty | BindingFlags.GetField | BindingFlags.GetProperty;
#endif
        }

        #endregion

        #region Implementation of IActivator

        /// <summary>
        /// Creates new instance of the specified service, and injects values into properties, fields, methods.
        /// </summary>
        /// <param name="constructorInfo">The specified <see cref="ConstructorInfo"/>.</param>
        /// <param name="bindingContext">The specified <see cref="IBindingContext"/>.</param>
        /// <returns>An instance of the service.</returns>
        public object Activate(ConstructorInfo constructorInfo, IBindingContext bindingContext)
        {
            if (constructorInfo == null)
                throw ActivatorException.CreateActivatorNullConstructorException(bindingContext);
            CurrentContext = bindingContext;
            try
            {
                object activate = Activate(constructorInfo);
                if (activate == null)
                    return null;
                Inject(constructorInfo.DeclaringType, ref activate);
                return activate;
            }
            catch (ActivatorException)
            {
                throw;
            }
            catch (BindingException)
            {
                throw;
            }
            catch (CyclicDependencyException)
            {
                throw;
            }
            catch (Exception exception)
            {
                throw ActivatorException.CreateActivatorException(bindingContext, constructorInfo, exception);
            }
            finally
            {
                CurrentContext = null;
            }
        }

        /// <summary>
        /// Injects values into properties, fields, methods in the specified object.
        /// </summary>
        /// <param name="target">The specified target for inject.</param>
        /// <param name="context">The specified <see cref="IBindingContext"/>.</param>
        public void Activate<T>(ref T target, IBindingContext context)
        {
            Validate.ArgumentNotNull(target, "target");
            Validate.ArgumentNotNull(context, "context");
            try
            {
                CurrentContext = context;
                Inject(target.GetType(), ref target);
            }
            catch (Exception exception)
            {
                throw ActivatorException.CreateActivatorException(target, context, exception);
            }
            finally
            {
                CurrentContext = null;
            }
        }

        #endregion

        #region Properties

#if !NETFX_CORE
        /// <summary>
        ///     Gets or sets the <see cref="BindingFlags" /> to search.
        /// </summary>
        public BindingFlags BindingFlags
        {
            get { return _bindingFlags; }
            set
            {
                if (value == _bindingFlags) return;
                _bindingFlags = value;
                _cachedMethodModel = new CacheInfoModel<MethodInfo>();
                _cachedPropertyModel = new CacheInfoModel<PropertyInfo>();
            }
        }
#endif

        /// <summary>
        ///     Gets the current <see cref="IBindingContext" />.
        /// </summary>
        protected IBindingContext CurrentContext { get; private set; }

        #endregion

        #region Abstract or virtual

        /// <summary>
        ///     Creates a new instance of the specified service.
        /// </summary>
        /// <param name="constructorInfo">
        ///     The specified <see cref="ConstructorInfo" />.
        /// </param>
        protected abstract object Activate(ConstructorInfo constructorInfo);

        /// <summary>
        ///     Injects values into properties, fields, methods in the specified object.
        /// </summary>
        /// <param name="targetType">
        ///     The specified target <see cref="Type" />.
        /// </param>
        /// <param name="target">The specified target for inject.</param>
        protected abstract void Inject<T>(Type targetType, ref T target);

        #endregion

        #region Protected methods

        /// <summary>
        ///     Checks the specified <see cref="MemberInfo" /> to inject.
        /// </summary>
        /// <param name="attributeType">The specified attribute type that use for inject.</param>
        /// <param name="memberInfo">
        ///     The specified <see cref="MemberInfo" />.
        /// </param>
        /// <returns>
        ///     If <c>true</c> need inject;otherwise <c>false</c>.
        /// </returns>
        protected virtual bool IsNeedInject(Type attributeType, MemberInfo memberInfo)
        {
            return memberInfo.IsDefined(attributeType, true) || memberInfo.IsDefined(typeof(ResolverAttribute), true);
        }

        /// <summary>
        /// Indicates that the service has values to inject.
        /// </summary>
        /// <param name="service">The specified service.</param>
        /// <param name="attributeType">The specified attribute type that use for inject.</param>
        /// <returns>If <c>true</c> service have values for inject; otherwise <c>false</c>.</returns>
        protected virtual bool IsHasValuesToInject(Type service, Type attributeType)
        {
            return !_isEmpty || _cachedPropertyModel.AttributeType != attributeType || _cachedPropertyModel.Type != service;
        }

        /// <summary>
        ///     Gets a series of methods to inject.
        /// </summary>
        /// <param name="service">The specified service type.</param>
        /// <param name="attributeType">The specified attribute to inject.</param>
        /// <returns>A series of methods to inject.</returns>
        protected virtual IList<MethodInfo> GetMethodsToInject(Type service, Type attributeType)
        {
            if (_cachedMethodModel.AttributeType == attributeType && _cachedMethodModel.Type == service)
                return _cachedMethodModel.CachedInfos;
            _cachedMethodModel = new CacheInfoModel<MethodInfo>(service, attributeType);
#if NETFX_CORE
            var methods = service.GetMethods();
#else
            MethodInfo[] methods = service.GetMethods(BindingFlags);
#endif
            for (int i = 0; i < methods.Length; i++)
            {
                MethodInfo methodInfo = methods[i];
                if (!IsNeedInject(attributeType, methodInfo)) continue;
                _cachedMethodModel.CachedInfos.Add(methodInfo);
            }
            UpdateFlag();
            return _cachedMethodModel.CachedInfos;
        }

        /// <summary>
        ///     Gets a series of properties to inject.
        /// </summary>
        /// <param name="service">The specified service type.</param>
        /// <param name="attributeType">The specified attribute to inject.</param>
        /// <returns>A series of properties tp inject.</returns>
        protected virtual IList<PropertyInfo> GetPropertiesToInject(Type service, Type attributeType)
        {
            if (_cachedPropertyModel.AttributeType == attributeType && _cachedPropertyModel.Type == service)
                return _cachedPropertyModel.CachedInfos;
            _cachedPropertyModel = new CacheInfoModel<PropertyInfo>(service, attributeType);
#if NETFX_CORE
            var properties = service.GetProperties();
#else
            PropertyInfo[] properties = service.GetProperties(BindingFlags);
#endif
            for (int i = 0; i < properties.Length; i++)
            {
                PropertyInfo propertyInfo = properties[i];
                if (!IsNeedInject(attributeType, propertyInfo)) continue;
                _cachedPropertyModel.CachedInfos.Add(propertyInfo);
            }
            UpdateFlag();
            return _cachedPropertyModel.CachedInfos;
        }

        private void UpdateFlag()
        {
            _isEmpty = (_cachedMethodModel.CachedInfos != null && _cachedMethodModel.CachedInfos.Count == 0) &&
                       (_cachedPropertyModel.CachedInfos != null && _cachedPropertyModel.CachedInfos.Count == 0);
        }

        #endregion
    }
}